package top.xunfong.file.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import top.xunfong.common.core.constant.redisKey.RedisKeyConstants;
import top.xunfong.common.core.exception.ServiceException;
import top.xunfong.file.constant.FileConstants;
import top.xunfong.file.domain.BreakpointChunk;
import top.xunfong.file.domain.BreakpointMetadata;
import top.xunfong.file.domain.dto.BreakpointDTO;
import top.xunfong.file.domain.vo.BreakpointCheckVO;
import top.xunfong.file.domain.vo.BreakpointMergeVO;
import top.xunfong.file.domain.vo.FileVO;
import top.xunfong.file.service.FileService;
import top.xunfong.file.util.FileUploadUtil;
import top.xunfong.redis.annotation.RedisLockAnnotation;
import top.xunfong.redis.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ExecutorService;


@Primary
@Slf4j
@Service
public class LocalFileService implements FileService {

    @Autowired
    private RedisService redisService;

    @Autowired
    @Qualifier(value = "deleteFileTaskExecutor")
    private ExecutorService deleteFileTaskExecutor;

    @Value("${file.baseUrl:D:/upload/}")
    private String BaseDir;

    @Override
    public FileVO upload(MultipartFile file) throws IOException {
        // 文件重命名（包含路径）
        String filename = FileUploadUtil.extractFilename(file);
        // 文件保存到本地
        File localFile = FileUploadUtil.fileToBaseDir(file, BaseDir + "/" + filename);
        return FileVO.builder().url(filename).name(localFile.getName()).originalName(file.getName()).build();
    }

    @Override
    @RedisLockAnnotation(key = "'" + RedisKeyConstants.BREAKPOINT_CHUNK_LOCK_KEY + "' + #dto.md5 + ':' + #dto.chunkIndex")
    public void breakpoint(BreakpointDTO dto, MultipartFile file) throws IOException {
        if (ObjectUtil.isNotNull(dto.getChunkIndex()) && dto.getChunkIndex().equals(0)) {
            // 创建断点续传文件
            breakpointCreate(dto);
        }
        if (ObjectUtil.isNotNull(file)) {
            breakpointChunk(dto, file);
        }
    }

    @Override
    public BreakpointCheckVO breakpointCheck(BreakpointDTO dto) {
        final String cacheKey = RedisKeyConstants.BREAKPOINT_METADATA_KEY + dto.getMd5();
        final String chunkKey = RedisKeyConstants.BREAKPOINT_CHUNK_KEY + dto.getMd5();
        BreakpointMetadata cacheObject = redisService.getCacheObject(cacheKey);
        if (ObjectUtil.isNotNull(cacheObject)) {
            if (cacheObject.getMd5().equals(dto.getMd5())) {
                List<BreakpointChunk> cacheList = redisService.getCacheList(chunkKey);
                List<Integer> chunks = new ArrayList<>();
                for (int i = 0; i < cacheList.size(); i++) {
                    BreakpointChunk breakpointChunk = cacheList.get(i);
                    if (ObjectUtil.isNotNull(breakpointChunk.getChunkIndex())) {
                        chunks.add(i);
                    }
                }
                return BreakpointCheckVO.builder().chunks(chunks).build();
            }
        }
        return BreakpointCheckVO.builder().chunks(new ArrayList<>()).build();
    }

    /**
     * 创建断点续传文件
     *
     * @param dto
     */
    private void breakpointCreate(BreakpointDTO dto) {
        // 创建断点续传文件
        redisService.setCacheObject(RedisKeyConstants.BREAKPOINT_METADATA_KEY + dto.getMd5(),
                BeanUtil.copyProperties(dto, BreakpointMetadata.class),
                FileConstants.BREAKPOINT_EXPIRE_TIME, FileConstants.BREAKPOINT_EXPIRE_TIME_UNIT);

    }

    /**
     * 断点续传分片上传
     *
     * @param dto
     * @param file
     */
    private void breakpointChunk(BreakpointDTO dto, MultipartFile file) throws IOException {
        if (ObjectUtil.isNull(file)) {
            return;
        }
        final String cacheKey = RedisKeyConstants.BREAKPOINT_CHUNK_KEY + dto.getMd5();
        List<BreakpointChunk> cacheList = redisService.getCacheList(cacheKey, Long.valueOf(dto.getChunkIndex()));
        if (CollUtil.isNotEmpty(cacheList) && ObjectUtil.isNotNull(cacheList.get(0).getChunkIndex())) {
            // 分片已存在，跳过
            return;
        }
        // 文件存储本地
        // 文件重命名（包含路径）
        String filename = FileUploadUtil.getUniversalFilePath(dto.getMd5(), UUID.randomUUID().toString());
        // 文件保存到本地
        File localFile = FileUploadUtil.fileToBaseDir(file, BaseDir + "/" + filename);

        BreakpointChunk breakpointChunk = BeanUtil.copyProperties(dto, BreakpointChunk.class);
        breakpointChunk.setLocalFilePath(filename);
        // 文件相关信息存储到redis中
        if (!redisService.hasKey(cacheKey)) {
            // 锁的值，随机生成
            String lockValue = UUID.randomUUID().toString();

            if (redisService.getRedisLock(RedisKeyConstants.BREAKPOINT_LOCK_KEY, lockValue, RedisKeyConstants.BREAKPOINT_LOCK_TIME)) {
                redisService.setCacheList(cacheKey, new ArrayList<>(Collections.nCopies(dto.getTotalSize(), new BreakpointChunk())));
                // 释放锁
                redisService.releaseRedisLock(RedisKeyConstants.BREAKPOINT_LOCK_KEY, lockValue);
            }
            // 单模块部署解决方案
//            synchronized (this) {
//                if (!redisService.hasKey(cacheKey)) {
//                    redisService.setCacheList(cacheKey, new ArrayList<>(Collections.nCopies(dto.getTotalSize(), "")));
//                }
//            }
        }
        redisService.setCacheList(cacheKey, breakpointChunk, dto.getChunkIndex());
        // 设置过期时间
        redisService.expire(cacheKey, FileConstants.BREAKPOINT_EXPIRE_TIME, FileConstants.BREAKPOINT_EXPIRE_TIME_UNIT);
    }

    /**
     * 断点续传合并请求
     *
     * @param dto
     */
    @Override
    @RedisLockAnnotation(key = "'" + RedisKeyConstants.BREAKPOINT_MERGE_LOCK_KEY + "'" + " + #dto.md5")
    public BreakpointMergeVO breakpointComplete(BreakpointDTO dto) {
        // 获取redis锁
        // 完成后，合并所有的断点
        final String chunkKey = RedisKeyConstants.BREAKPOINT_CHUNK_KEY + dto.getMd5();
        final String metadataKey = RedisKeyConstants.BREAKPOINT_METADATA_KEY + dto.getMd5();
        BreakpointMetadata metadata = redisService.getCacheObject(metadataKey);
        // 所有分片信息, 如果数据量很大可以考虑分页读取
        List<BreakpointChunk> cacheList = redisService.getCacheList(chunkKey);

        String filename = FileUploadUtil.getUniversalFilePath(FileUploadUtil.getFileName(FileNameUtil.getSuffix(metadata.getFileName())));
        String fileUrl = FileUploadUtil.concatPath(BaseDir, filename);
        // 检查文件夹是否存在，如果不纯在，则创建
        String parent = FileUtil.getParent(fileUrl, 1);
        FileUtil.mkdir(parent);

        try (FileOutputStream fos = new FileOutputStream(fileUrl, true)) {
            for (BreakpointChunk chunk : cacheList) {
                if (chunk != null && StrUtil.isNotBlank(chunk.getLocalFilePath())) {
                    String chunkFilePath = BaseDir + "/" + chunk.getLocalFilePath();
                    byte[] bytes = FileUtil.readBytes(chunkFilePath);
                    fos.write(bytes);
                }
            }
        } catch (IOException e) {
            log.error("合并异常", e);
            throw new ServiceException("合并异常");
        } finally {
            deleteFileByMd5(dto.getMd5());
        }
        // 校验文件完整性
        if (!FileUploadUtil.checkFileMD5(FileUploadUtil.concatPath(BaseDir, filename), metadata.getMd5())) {
            log.error("文件校验失败");
            throw new ServiceException("文件完整性校验失败, 请重新上传");
        }
        return BreakpointMergeVO.builder()
                .url(filename)
                .name(FileUtil.getName(filename))
                .originalName(metadata.getFileName())
                .build();
    }

    @Override
    @RedisLockAnnotation(key = "'" + RedisKeyConstants.BREAKPOINT_DELETE_LOCK_KEY + "'" + " + #md5")
    public void deleteFileByMd5(String md5) {
        final String chunkKey = RedisKeyConstants.BREAKPOINT_CHUNK_KEY + md5;
        final String metadataKey = RedisKeyConstants.BREAKPOINT_METADATA_KEY + md5;
        deleteFileTaskExecutor.execute(() -> deleteChunks(chunkKey, metadataKey));
    }

    /**
     * 删除所有分片信息(删除整个文件夹)
     */
    private void deleteChunks(String chunkKey, String metadataKey) {
        List<BreakpointChunk> cacheList = redisService.getCacheList(chunkKey);
        if (ObjectUtil.isNotNull(cacheList)) {
            // 删除文件夹，可能出现跨天的情况，可能会删除多个文件夹
            List<String> list = cacheList.stream().filter(x -> StrUtil.isNotBlank(x.getLocalFilePath())).map(x -> {
                // 获取文件夹路径
                return FileUtil.getParent(FileUploadUtil.concatPath(BaseDir, x.getLocalFilePath()), 1);
            }).distinct().toList();
            for (String url : list) {
                FileUtil.del(url);
            }
        }
        redisService.deleteObject(Arrays.asList(chunkKey, metadataKey));
    }
}
